/*
 * Copyright (c) 2013-2015 Charkey. All rights reserved.
 *
 * This software is the confidential and proprietary information of Charkey.
 * You shall not disclose such Confidential Information and shall use it only
 * in accordance with the terms of the agreements you entered into with Charkey.
 *
 * Charkey MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE SOFTWARE,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT.
 *
 * Charkey SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING,
 * MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
 */
package cn.simastudio.talos.core.utils;

import cn.simastudio.talos.common.utils.codec.MD5Utils;
import cn.simastudio.talos.core.constants.MessageCode;
import cn.simastudio.talos.core.exception.file.*;
import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import static org.apache.commons.fileupload.FileUploadBase.FileSizeLimitExceededException;

/**
 * Usage: 文件上传工具类
 *
 * @author Charkey
 * @date 2015/7/10 21:11
 */
public class FileUploadUtils {

    /**
     * 默认文件最大大小：50M
     */
    public static final long DEFAULT_MAX_SIZE = 52428800;

    /**
     * 日期路径格式
     */
    public static final String DATE_PATH_PATTERN = "yyyy/MM/dd";

    /**
     * 默认的文件名最大长度
     */
    public static final int DEFAULT_FILE_NAME_LENGTH = 200;

    /**
     * 图片扩展名列表
     */
    public static final String[] IMAGE_EXTENSION = {
            "bmp", "gif", "jpg", "jpeg", "png"
    };

    /**
     * flash文件扩展名列表
     */
    public static final String[] FLASH_EXTENSION = {
            "swf", "flv"
    };

    /**
     * 多媒体文件扩展名列表
     */
    public static final String[] MEDIA_EXTENSION = {
            "swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg", "asf", "rm", "rmvb"
    };

    /**
     * 默认允许上传的文件类型
     */
    public static final String[] DEFAULT_ALLOWED_EXTENSION = {
            //图片
            "bmp", "gif", "jpg", "jpeg", "png",
            //word excel powerpoint
            "doc", "docx", "xls", "xlsx", "ppt", "pptx",
            "html", "htm", "txt",
            //压缩文件
            "rar", "zip", "gz", "bz2",
            //pdf
            "pdf"
    };

    private static final Logger LOGGER = LoggerFactory.getLogger(FileUploadUtils.class);

    /**
     * 默认上传的地址
     */
    // Sima: 文件上传工具设置的默认上传地址为upload
    private static String defaultBaseDir = "upload";

    private static int counter = 0;

    /**
     * 获得默认上传地址
     *
     * @return 默认上传地址
     */
    public static String getDefaultBaseDir() {
        return defaultBaseDir;
    }

    /**
     * 设置默认上传地址
     *
     * @param defaultBaseDir 默认上传地址
     */
    public static void setDefaultBaseDir(String defaultBaseDir) {
        FileUploadUtils.defaultBaseDir = defaultBaseDir;
    }

    /**
     * 以默认配置进行文件上传
     *
     * @param request        当前请求
     * @param file           上传的文件
     * @param newFileName    新文件名
     * @param customBaseDir  自定义上传根目录
     * @param needDatePath   是否需要日期目录
     * @param needRandomName 是否需要随机文件名前缀
     * @param result         上传结果
     * @return 以上传地址开始文件路径
     */
    public static String upload(HttpServletRequest request, MultipartFile file, String newFileName,
                                String customBaseDir, boolean needDatePath, boolean needRandomName, BindingResult result) {
        return upload(request, file, newFileName, customBaseDir, DEFAULT_ALLOWED_EXTENSION, needDatePath, needRandomName, result);
    }

    /**
     * 以默认配置进行文件上传
     *
     * @param request          当前请求
     * @param file             上传的文件
     * @param newFileName      新文件名
     * @param customBaseDir    自定义上传根目录
     * @param allowedExtension 允许上传的文件类型
     * @param needDatePath     是否需要日期目录
     * @param needRandomName   是否需要随机文件名前缀
     * @param result           上传结果
     * @return 以上传地址开始文件路径
     */
    public static String upload(HttpServletRequest request, MultipartFile file, String newFileName,
                                String customBaseDir, String[] allowedExtension, boolean needDatePath, boolean needRandomName, BindingResult result) {
        try {
            String uploadBaseDir = getDefaultBaseDir();
            // 如果customBaseDir不为空，则上传到这个指定的目录
            if (StringUtils.isNotEmpty(customBaseDir)) {
                uploadBaseDir = customBaseDir;
            }
            return upload(request, uploadBaseDir, file, newFileName, allowedExtension, DEFAULT_MAX_SIZE, needDatePath, needRandomName);
        } catch (IOException e) {
            LOGGER.error("Upload failed! Server error! Message: {}", e);
            result.reject(MessageCode.UPLOAD_SERVER_ERROR);
        } catch (InvalidImageExtensionException e) {
            result.reject(MessageCode.UPLOAD_NOT_ALLOW_IMAGE_EXTENSION);
        } catch (InvalidFlashExtensionException e) {
            result.reject(MessageCode.UPLOAD_NOT_ALLOW_FLASH_EXTENSION);
        } catch (InvalidMediaExtensionException e) {
            result.reject(MessageCode.UPLOAD_NOT_ALLOW_MEDIA_EXTENSION);
        } catch (InvalidExtensionException e) {
            result.reject(MessageCode.UPLOAD_NOT_ALLOW_EXTENSION);
        } catch (FileUploadBase.FileSizeLimitExceededException e) {
            result.reject(MessageCode.UPLOAD_EXCEED_MAXSIZE);
        } catch (FileNameLengthLimitExceededException e) {
            result.reject(MessageCode.UPLOAD_FILENAME_EXCEED_LENGTH);
        }
        return null;
    }

    /**
     * @param request          当前请求 从请求中提取 应用上下文根
     * @param baseDir          相对应用的基目录
     * @param file             上传的文件
     * @param newFileName      新文件名（需要有扩展名）
     * @param allowedExtension 允许的文件类型 null 表示允许所有，默认值参考{@link FileUploadUtils#DEFAULT_ALLOWED_EXTENSION}
     * @param maxSize          最大上传的大小 -1 表示不限制
     * @param needDatePath     是否需要日期目录
     * @param needRandomName   是否需要随机文件名前缀
     * @return 以上传地址开始文件路径
     * @throws InvalidExtensionException            如果MIME类型不允许
     * @throws FileSizeLimitExceededException       如果超出最大大小
     * @throws FileNameLengthLimitExceededException 文件名太长
     * @throws IOException                          比如读写文件出错时
     * @throws InvalidExtensionException            如果MIME类型不允许
     * @throws FileSizeLimitExceededException       如果超出最大大小
     * @throws IOException                          比如读写文件出错时
     * @throws FileNameLengthLimitExceededException 文件名太长
     */
    public static String upload(HttpServletRequest request, String baseDir, MultipartFile file, String newFileName,
                                String[] allowedExtension, long maxSize, boolean needDatePath, boolean needRandomName)
            throws InvalidExtensionException, FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException {
        int fileNameLength = file.getOriginalFilename().length();
        if (fileNameLength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) {
            throw new FileNameLengthLimitExceededException(file.getOriginalFilename(), fileNameLength, FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
        }
        assertAllowed(file, allowedExtension, maxSize);
        // 这边，如果newFileName不为空，则会将文件重命名为newFileName
        String filename = extractFilename(file, newFileName, baseDir, needDatePath, needRandomName);
        File desc = getAbsoluteFile(extractUploadDir(request), filename);
        file.transferTo(desc);
        return filename;
    }

    /**
     * <p>获得文件的绝对路径，会更新修正项目路径分隔符</p>
     * <p>如"//"改为"/"，"\/"改为"/"，根据系统类型自动变更路径分隔符</p>
     *
     * @param uploadDir 上传目录
     * @param filename  文件名
     * @return 文件
     * @throws IOException
     */
    public static File getAbsoluteFile(String uploadDir, String filename) throws IOException {
        uploadDir = FilenameUtils.normalizeNoEndSeparator(uploadDir);
        filename = FilenameUtils.normalizeNoEndSeparator(filename);
        File desc = new File(uploadDir + File.separator + filename);
        if (!desc.getParentFile().exists()) {
            // 注意是mkdirs，创建此抽象路径名指定的目录，包括创建必需但不存在的父目录
            boolean mkRes = desc.getParentFile().mkdirs();
            if (!mkRes) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.error("Make directories failed, path: {}", desc.getParentFile());
                }
            }
        }
        if (!desc.exists()) {
            boolean createRes = desc.createNewFile();
            if (!createRes) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.error("New file create failed, file: {}", desc);
                }
            }
        }
        return desc;
    }

    /**
     * 获得以baseDir开始的文件路径
     *
     * @param file           文件
     * @param newFileName    新文件名称
     * @param baseDir        默认上传的地址
     * @param needDatePath   是否需要日期目录
     * @param needRandomName 是否需要随机文件名前缀
     * @return 以baseDir开始的文件路径
     * @throws UnsupportedEncodingException
     */
    public static String extractFilename(MultipartFile file, String newFileName, String baseDir, boolean needDatePath, boolean needRandomName)
            throws UnsupportedEncodingException {
        // 文件的原始名称（包括文件扩展名），如 demo.jpeg
        String filename = file.getOriginalFilename();
        // 如果newFileName参数不为空，则设置新文件名
        if (StringUtils.isNotEmpty(newFileName)) {
            filename = newFileName;
        }
        int slashIndex = filename.indexOf("/");
        // 获得文件名称：去掉路径中的"/"
        if (slashIndex >= 0) {
            filename = filename.substring(slashIndex + 1);
        }
        if (needDatePath) {
            filename = baseDir + File.separator + getDatePath() + File.separator + encodingFilename(filename, needRandomName);
        } else {
            filename = baseDir + File.separator + encodingFilename(filename, needRandomName);
        }
        return FilenameUtils.normalizeNoEndSeparator(filename);
    }

    /**
     * 编码文件名,默认格式为：
     * 1、'_'替换为 ' '
     * 2、hex(md5(filename + now nano time + counter++))
     * 3、[2]_原始文件名
     *
     * @param filename       文件名
     * @param needRandomName 是否需要随机文件名前缀
     * @return 文件名
     */
    private static String encodingFilename(String filename, boolean needRandomName) {
        if (needRandomName) {
            filename = filename.replace("_", " ");
            filename = MD5Utils.md5Digest(filename + System.nanoTime() + counter++) + "_" + filename;
            return filename;
        }
        return filename;
    }

    /**
     * 日期路径 即年/月/日  如2013/01/03
     *
     * @return 日期路径
     */
    private static String getDatePath() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(DATE_PATH_PATTERN));
    }

    /**
     * 是否允许文件上传
     *
     * @param file             上传的文件
     * @param allowedExtension 文件类型  null 表示允许所有
     * @param maxSize          最大大小 字节为单位 -1表示不限制
     * @throws InvalidExtensionException      如果MIME类型不允许
     * @throws FileSizeLimitExceededException 如果超出最大大小
     */
    public static void assertAllowed(MultipartFile file, String[] allowedExtension, long maxSize)
            throws InvalidExtensionException, FileSizeLimitExceededException {
        String filename = file.getOriginalFilename();
        String extension = FilenameUtils.getExtension(file.getOriginalFilename());
        if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) {
            if (allowedExtension == IMAGE_EXTENSION) {
                throw new InvalidImageExtensionException(allowedExtension, extension, filename);
            } else if (allowedExtension == FLASH_EXTENSION) {
                throw new InvalidFlashExtensionException(allowedExtension, extension, filename);
            } else if (allowedExtension == MEDIA_EXTENSION) {
                throw new InvalidMediaExtensionException(allowedExtension, extension, filename);
            } else {
                throw new InvalidExtensionException(allowedExtension, extension, filename);
            }
        }
        long size = file.getSize();
        if (maxSize != -1 && size > maxSize) {
            throw new FileSizeLimitExceededException("not allowed upload upload", size, maxSize);
        }
    }

    /**
     * 判断MIME类型是否是允许的MIME类型
     *
     * @param extension        扩展名
     * @param allowedExtension 允许的扩展名列表
     * @return true允许，false不允许
     */
    public static boolean isAllowedExtension(String extension, String[] allowedExtension) {
        for (String str : allowedExtension) {
            if (str.equalsIgnoreCase(extension)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 提取上传的根目录 默认是应用的根
     *
     * @param request request
     * @return 上传的根目录
     */
    public static String extractUploadDir(HttpServletRequest request) {
        return request.getServletContext().getRealPath("/");
    }

    /**
     * 删除文件
     *
     * @param request  request
     * @param filename 文件名
     * @throws IOException
     */
    public static void delete(HttpServletRequest request, String filename) throws IOException {
        if (StringUtils.isEmpty(filename)) {
            return;
        }
        File desc = getAbsoluteFile(extractUploadDir(request), filename);
        if (desc.exists()) {
            boolean delRes = desc.delete();
            if (!delRes) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.error("Delete file failed, file: {}", desc);
                }
            }
        }
    }
}